package org.testng.eclipse.launch;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.variables.VariablesPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.launching.AbstractJavaLaunchConfigurationDelegate;
import org.eclipse.jdt.launching.ExecutionArguments;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMRunner;
import org.eclipse.jdt.launching.SocketUtil;
import org.eclipse.jdt.launching.VMRunnerConfiguration;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.testng.TestNG;
import org.testng.TestNGCommandLineArgs;
import org.testng.eclipse.TestNGPlugin;
import org.testng.eclipse.ui.util.Utils;
import org.testng.eclipse.util.ClassMethodsSuite;
import org.testng.eclipse.util.JDTUtil;
import org.testng.eclipse.util.PreferenceStoreUtil;
import org.testng.eclipse.util.ResourceUtil;
import org.testng.remote.RemoteTestNG;
import org.testng.xml.LaunchSuite;

public class TestNGLaunchConfigurationDelegate extends
		AbstractJavaLaunchConfigurationDelegate {

	private static void ppp(final Object msg) {
		TestNGPlugin.log(new Status(IStatus.INFO, TestNGPlugin.PLUGIN_ID, 1,
				String.valueOf(msg), null));
	}

	private boolean fKeepAlive;
	private int fPort;
	private IMember[] fTestElements;

	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.AbstractJavaLaunchConfigurationDelegate#getClasspath(org.eclipse.debug.core.ILaunchConfiguration)
	 */
	public String[] getClasspath(ILaunchConfiguration configuration) throws CoreException {
		URL url = Platform.getBundle(TestNGPlugin.PLUGIN_ID).getEntry("/"); //$NON-NLS-1$
		IJavaProject jproject = getJavaProject(configuration);
		String javaVersion = JDTUtil.getProjectVMVersion(jproject); 

		String[] cp = super.getClasspath(configuration);
		String[] classPath = null;
		String testngJarLocation = getTestNGLibraryVersion(javaVersion);
		String testngJarName = testngJarLocation.indexOf('/') != -1 ? testngJarLocation
				.substring(testngJarLocation.indexOf('/') + 1)
				: testngJarLocation;
		boolean donotappendjar = false;
		String projectName = getJavaProjectName(configuration);
		if (null != projectName) {
			donotappendjar = TestNGPlugin.getPluginPreferenceStore()
					.getUseProjectJar(projectName);
		}

		int addedSize = 2;
		if (donotappendjar) {
			addedSize = 1;
		} else {
			for (int i = 0; i < cp.length; i++) {
				if (cp[i].endsWith(testngJarName)) {
					addedSize = 1;
					break;
				}
			}
		}

		try {
			if (Platform.inDevelopmentMode()) {

				// we first try the bin output folder
				List entries = new ArrayList();

				try {
					entries.add(FileLocator.toFileURL(
							new URL(url, "build/classes")).getFile()); //$NON-NLS-1$
				} catch (IOException e3) {
					try {
						entries.add(FileLocator.toFileURL(
								new URL(url, "eclipse-testng.jar")).getFile()); //$NON-NLS-1$
					} catch (IOException e4) {
						;
					}
				}
				if (addedSize == 2) {
					entries.add(FileLocator.toFileURL(
							new URL(url, testngJarLocation)).getFile()); //$NON-NLS-1$
				}

				Assert.isTrue(entries.size() == addedSize,
						"Required JARs available"); //$NON-NLS-1$

				classPath = new String[cp.length + entries.size()];

				Object[] jea = entries.toArray();
				System.arraycopy(cp, 0, classPath, addedSize, cp.length);
				System.arraycopy(jea, 0, classPath, 0, jea.length);
			} else {
				classPath = new String[cp.length + addedSize];
				System.arraycopy(cp, 0, classPath, addedSize, cp.length);
				classPath[0] = FileLocator.toFileURL(
						new URL(url, "eclipse-testng.jar")).getFile(); //$NON-NLS-1$

				if (addedSize == 2) {
					classPath[1] = FileLocator.toFileURL(
							new URL(url, testngJarLocation)).getFile();
				}
			}
		} catch (IOException ioe) {
			TestNGPlugin.log(ioe);
			abort("Cannot create runtime classpath", ioe, 1000); //$NON-NLS-1$
		}

		return classPath;
	}

	/**
	 * Evaluates all test elements selected by the given launch configuration.
	 * The elements are of type {@link IType} or {@link IMethod}. At the moment
	 * it is only possible to run a single method or a set of types, but not
	 * mixed or more than one method at a time.
	 * 
	 * @param configuration
	 *            the launch configuration to inspect
	 * @param monitor
	 *            the progress monitor
	 * @return returns all types or methods that should be ran
	 * @throws CoreException
	 *             an exception is thrown when the search for tests failed
	 */
	protected IMember[] evaluateTests(ILaunchConfiguration configuration,
			IProgressMonitor monitor) throws CoreException {
		IJavaProject javaProject = getJavaProject(configuration);

		IJavaElement testTarget = getTestTarget(configuration, javaProject);
		String testMethodName = configuration.getAttribute(
				TestNGLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, ""); //$NON-NLS-1$
		if (testMethodName.length() > 0) {
			if (testTarget instanceof IType) {
				return new IMember[] { ((IType) testTarget).getMethod(
						testMethodName, new String[0]) };
			}
		}
		HashSet result = new HashSet();
		TestSearchEngine.getFinder().findTestsInContainer(testTarget, result,
				monitor);
		if (result.isEmpty()) {
			informAndAbort("No tests found", null,
					IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE);
		}
		return (IMember[]) result.toArray(new IMember[result.size()]);
	}

	private Display getDisplay() {
		Display display;
		display = Display.getCurrent();
		if (display == null)
			display = Display.getDefault();
		return display;
	}

	private String getTestNGLibraryVersion(final String javaVersion) {
		String testngLib = null;
		if (isJDK15(javaVersion)) {
			testngLib = ResourceUtil.getString("TestNG.jdk15.library"); //$NON-NLS-1$
		} else {
			testngLib = ResourceUtil.getString("TestNG.jdk14.library"); //$NON-NLS-1$
		}

		return testngLib;
	}

	private final IJavaElement getTestTarget(
			ILaunchConfiguration configuration, IJavaProject javaProject)
			throws CoreException {
		String containerHandle = configuration.getAttribute(
				TestNGLaunchConfigurationConstants.ATTR_TEST_CONTAINER, ""); //$NON-NLS-1$
		if (containerHandle.length() != 0) {
			IJavaElement element = JavaCore.create(containerHandle);
			if (element == null || !element.exists()) {
				informAndAbort(
						"The input element of the launch configuration does not exist",
						null,
						IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE);
			}
			return element;
		}
		String testTypeName = configuration.getAttribute(
				IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, ""); //$NON-NLS-1$
		if (testTypeName.length() != 0) {
			testTypeName = performStringSubstitution(testTypeName);
			IType type = javaProject.findType(testTypeName);
			if (type != null && type.exists()) {
				return type;
			}
		}
		informAndAbort(
				"The input type of the launch configuration does not exist",
				null,
				IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE);
		return null; // not reachable
	}

	private void informAndAbort(String message, Throwable exception, int code)
			throws CoreException {
		IStatus status = new Status(IStatus.INFO, TestNGPlugin.PLUGIN_ID, code,
				message, exception);
		if (showStatusMessage(status)) {
			// Status message successfully shown
			// -> Abort with INFO exception
			// -> Worker.run() will not write to log
			throw new CoreException(status);
		} else {
			// Status message could not be shown
			// -> Abort with original exception
			// -> Will write WARNINGs and ERRORs to log
			abort(message, exception, code);
		}
	}

	/**
	 * @param javaVersion
	 * @return
	 */
	private boolean isJDK15(String javaVersion) {
		return TestNG.JDK_ANNOTATION_TYPE.equalsIgnoreCase(javaVersion)
				|| javaVersion.startsWith("1.5")
				|| javaVersion.startsWith("1.6");
	}
	
	/**
	 * Performs a check on the launch configuration's attributes. If an attribute contains an invalid value, a {@link CoreException}
	 * with the error is thrown.
	 * 
	 * @param configuration the launch configuration to verify
	 * @param launch the launch to verify
	 * @param monitor the progress monitor to use
	 * @throws CoreException an exception is thrown when the verification fails
	 */
	protected void preLaunchCheck(ILaunchConfiguration configuration, ILaunch launch, IProgressMonitor monitor) throws CoreException {
		try {
			IJavaProject javaProject= getJavaProject(configuration);
			if ((javaProject == null) || !javaProject.exists()) {
				informAndAbort("Invalid project specified.", null, IJavaLaunchConfigurationConstants.ERR_NOT_A_JAVA_PROJECT); 
			}
//			if (!TestSearchEngine.hasTestCaseType(javaProject)) {
//				informAndAbort(JUnitMessages.JUnitLaunchConfigurationDelegate_error_junitnotonpath, null, IJUnitStatusConstants.ERR_JUNIT_NOT_ON_PATH);
//			}

//			boolean isJUnit4Configuration= TestKindRegistry.JUNIT4_TEST_KIND_ID.equals(testKind.getId());
//			if (isJUnit4Configuration && ! TestSearchEngine.hasTestAnnotation(javaProject)) {
//				informAndAbort(JUnitMessages.JUnitLaunchConfigurationDelegate_error_junit4notonpath, null, IJUnitStatusConstants.ERR_JUNIT_NOT_ON_PATH);
//			}
		} finally {
			monitor.done();
		}
	}	
	
	/**
	 * Collects all VM and program arguments. Implementors can modify and add arguments.
	 * 
	 * @param configuration the configuration to collect the arguments for
	 * @param vmArguments a {@link List} of {@link String} representing the resulting VM arguments
	 * @param programArguments a {@link List} of {@link String} representing the resulting program arguments
	 * @exception CoreException if unable to collect the execution arguments
	 */
	protected void collectExecutionArguments(ILaunchConfiguration configuration, List/*String*/ vmArguments, List/*String*/ programArguments) throws CoreException {

		// add program & VM arguments provided by getProgramArguments and getVMArguments
		String pgmArgs= getProgramArguments(configuration);
		String vmArgs= getVMArguments(configuration);
		ExecutionArguments execArgs= new ExecutionArguments(vmArgs, pgmArgs);
		vmArguments.addAll(Arrays.asList(execArgs.getVMArgumentsArray()));
		programArguments.addAll(Arrays.asList(execArgs.getProgramArgumentsArray()));
		
		programArguments.add(TestNGCommandLineArgs.PORT_COMMAND_OPT);
		programArguments.add(String.valueOf(fPort));
		
		IJavaProject jproject = getJavaProject(configuration);
		String javaVersion = JDTUtil.getProjectVMVersion(jproject); 

		if (fKeepAlive)
			programArguments.add(0, "-keepalive"); //$NON-NLS-1$
				
		if (!isJDK15(javaVersion)) {
			List sourceDirs = JDTUtil.getSourceDirFileList(jproject);
			if (null != sourceDirs) {
				programArguments.add(TestNGCommandLineArgs.SRC_COMMAND_OPT);
				programArguments.add(Utils.toSinglePath(sourceDirs, ";")); //$NON-NLS-1$
			}
		}
		
		PreferenceStoreUtil storage = TestNGPlugin.getPluginPreferenceStore();
		programArguments.add(TestNGCommandLineArgs.OUTDIR_COMMAND_OPT);
		programArguments.add(storage.getOutputAbsolutePath(jproject).toOSString());		
		IProject project = jproject.getProject();
		
		String reporters = storage.getReporters(project.getName(), false);
		if (null != reporters && !"".equals(reporters)) {
			programArguments.add(TestNGCommandLineArgs.LISTENER_COMMAND_OPT);
			programArguments.add(reporters.replace(' ', ';'));
		}

		boolean disabledReporters = storage.hasDisabledListeners(project
				.getName(), false);
		if (disabledReporters) {
			programArguments.add(TestNGCommandLineArgs.USE_DEFAULT_LISTENERS);
			programArguments.add("false");
		}		
		
//	    if (type == TestNGLaunchConfigurationConstants.SUITE) {
//	        return createLaunchSuites(ijp.getProject(), getSuites(configuration));
//	      }		
		
		IMember[] testElements = fTestElements;
		
		File projectPathFile = new File(project.getLocation().toOSString());

		LaunchSuite launchSuite = null;
        
        
		int logLevel = 2;
				
		// a test name was specified just run the single test
		if (testElements.length == 1) {
			if (testElements[0] instanceof IMethod) {
				IMethod method= (IMethod) testElements[0];
				launchSuite = new ClassMethodsSuite(jproject,method.getDeclaringType().getFullyQualifiedName(), method.getElementName(),null,javaVersion ,logLevel);
			} else if (testElements[0] instanceof IType) {
				IType type= (IType) testElements[0];
				launchSuite = new ClassMethodsSuite(jproject,type.getFullyQualifiedName(), null,null,javaVersion,logLevel);
			} else {
				informAndAbort("Can only run types or single method", null, IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE); 
			}
		} else if (testElements.length > 1) {
			List classNames = new  ArrayList();
			for (int i= 0; i < testElements.length; i++) {
				if (testElements[i] instanceof IType) {
					IType type= (IType) testElements[i];
					classNames.add(type.getFullyQualifiedName());
				} else {
					informAndAbort("Can only run types or single method", null, IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE); 
				}
			}
			launchSuite = new ClassMethodsSuite(jproject,classNames,null, null, javaVersion,logLevel);			
		}

		File suiteFile = launchSuite.save(projectPathFile);

		if (launchSuite.isTemporary()) {
			suiteFile.deleteOnExit();
		}
	

		programArguments.add(suiteFile.getAbsolutePath());

//		launch.setAttribute(
//				TestNGLaunchConfigurationConstants.TEMP_SUITE_LIST, suiteFile.getAbsolutePath());
		
	}	
	
	/* (non-Javadoc)
	 * @see org.eclipse.jdt.launching.AbstractJavaLaunchConfigurationDelegate#verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration)
	 */
	public String verifyMainTypeName(ILaunchConfiguration configuration) throws CoreException {
		return RemoteTestNG.class.getName();
	}	
	
	/* (non-Javadoc)
	 * @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
	 */
	public synchronized void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException {
		if (monitor == null) {
			monitor = new NullProgressMonitor();
		}
		
		monitor.beginTask(MessageFormat.format("{0}...", new String[]{configuration.getName()}), 5); //$NON-NLS-1$
		// check for cancellation
		if (monitor.isCanceled()) {
			return;
		}
		
		try {
			if (mode.equals(TestNGLaunchConfigurationConstants.MODE_RUN_QUIETLY_MODE)) {
				launch.setAttribute(TestNGLaunchConfigurationConstants.ATTR_NO_DISPLAY, "true"); //$NON-NLS-1$
				mode = ILaunchManager.RUN_MODE;
			}
			
			monitor.subTask("Verifying launch attributes..."); 
			
			try {
				preLaunchCheck(configuration, launch, new SubProgressMonitor(monitor, 2));
			} catch (CoreException e) {
				if (e.getStatus().getSeverity() == IStatus.CANCEL) {
					monitor.setCanceled(true);
					return;
				}
				throw e;
			}
			// check for cancellation
			if (monitor.isCanceled()) {
				return;
			}		
			
			fKeepAlive= mode.equals(ILaunchManager.DEBUG_MODE) && configuration.getAttribute(TestNGLaunchConfigurationConstants.ATTR_KEEPRUNNING, false);
			fPort= evaluatePort();
			launch.setAttribute(TestNGLaunchConfigurationConstants.ATTR_PORT, String.valueOf(fPort));
				
			fTestElements= evaluateTests(configuration, new SubProgressMonitor(monitor, 1));

			String mainTypeName= verifyMainTypeName(configuration);
			IVMRunner runner= getVMRunner(configuration, mode);

			File workingDir = verifyWorkingDirectory(configuration);
			String workingDirName = null;
			if (workingDir != null) {
				workingDirName= workingDir.getAbsolutePath();
			}
			
			// Environment variables
			String[] envp= getEnvironment(configuration);
			
			ArrayList vmArguments= new ArrayList();
			ArrayList programArguments= new ArrayList();
			collectExecutionArguments(configuration, vmArguments, programArguments);
			
			// VM-specific attributes
			Map vmAttributesMap= getVMSpecificAttributesMap(configuration);
			
			// Classpath
			String[] classpath= getClasspath(configuration);
			
			// Create VM config
			VMRunnerConfiguration runConfig= new VMRunnerConfiguration(mainTypeName, classpath);
			runConfig.setVMArguments((String[]) vmArguments.toArray(new String[vmArguments.size()]));
			runConfig.setProgramArguments((String[]) programArguments.toArray(new String[programArguments.size()]));
			runConfig.setEnvironment(envp);
			runConfig.setWorkingDirectory(workingDirName);
			runConfig.setVMSpecificAttributesMap(vmAttributesMap);

			// Bootpath
			runConfig.setBootClassPath(getBootpath(configuration));
			
			// check for cancellation
			if (monitor.isCanceled()) {
				return;
			}		
			
			// done the verification phase
			monitor.worked(1);
			
			monitor.subTask("Creating source locator..."); 
			// set the default source locator if required
			setDefaultSourceLocator(launch, configuration);
			monitor.worked(1);		
			
			// Launch the configuration - 1 unit of work
			runner.run(runConfig, launch, monitor);
			
			// check for cancellation
			if (monitor.isCanceled()) {
				return;
			}
		} finally {
			fTestElements= null;
			monitor.done();
		}
	}
	
	
	private int evaluatePort() throws CoreException {
		int port= SocketUtil.findFreePort();
		if (port == -1) {
			informAndAbort("No socket available", null, IJavaLaunchConfigurationConstants.ERR_NO_SOCKET_AVAILABLE); 
		}
		return port;
	}	
	
	private final String performStringSubstitution(String testTypeName)
			throws CoreException {
		return VariablesPlugin.getDefault().getStringVariableManager()
				.performStringSubstitution(testTypeName);
	}

	private boolean showStatusMessage(final IStatus status) {
		final boolean[] success = new boolean[] { false };
		getDisplay().syncExec(new Runnable() {
			public void run() {
				Shell shell = TestNGPlugin.getActiveWorkbenchShell();
				if (shell == null)
					shell = getDisplay().getActiveShell();
				if (shell != null) {
					MessageDialog.openInformation(shell,
							"Problems Launching TestNG Tests", status
									.getMessage());
					success[0] = true;
				}
			}
		});
		return success[0];
	}
}
